为什么 TCP 建立连接需要三次握手
TCP(Transmission Control Protocol)是互联网中最核心的传输层协议之一,它提供了可靠的、面向连接的字节流服务。在 TCP 通信开始之前,通信双方需要通过「三次握手」建立连接。这个看似简单的过程,背后隐藏着精妙的设计考量。
一、TCP 连接的建立过程
1.1 三次握手的完整流程
sequenceDiagram participant C as 客户端 participant S as 服务端 C->>S: SYN seq=x S->>C: SYN ACK seq=y, ack=x+1 C->>S: ACK seq=x+1, ack=y+1 Note over C,S: 连接建立完成
三次握手的过程:
-
第一次握手(SYN):客户端发送一个 SYN 包(同步序列号),请求建立连接
-
第二次握手(SYN + ACK):服务端收到 SYN 后,返回 SYN+ACK 包,表示同意建立连接
-
第三次握手(ACK):客户端再发送一个 ACK 包,确认收到服务端的确认
1.2 抓包验证
使用 Wireshark 或 tcpdump 可以观察到实际的三次握手过程:
# 使用 tcpdump 抓取 TCP 握手包
tcpdump -i eth0 'tcp[tcpflags] & (tcp-syn|tcp-ack|tcp-fin) != 0'
# 观察到的三次握手
# 1. Client -> Server: SYN Seq=0
# 2. Server -> Client: SYN ACK Seq=0 Ack=1
# 3. Client -> Server: ACK Seq=1 Ack=1
bash二、为什么不能两次握手?
这是理解 TCP 设计的核心问题。直觉上,似乎确认和应答机制用两次就够了,为什么要画蛇添足地多一次?
2.1 两次握手无法保证可靠性
让我们分析一个具体场景:
sequenceDiagram participant C as 客户端 participant S as 服务端 Note over C,S: 场景:旧数据包延迟到达 C->>S: SYN seq=1000 (请求连接) S->>C: SYN ACK seq=2000, ack=1001 (同意连接) Note over C,S: 连接建立,发送数据 S->>C: 数据 (来自旧的延迟 SYN 请求) C->>S: ??? (困惑:这个数据是谁发的?)
问题的本质:TCP 是全双工通信,两次握手只能让一端确认另一端的存在,但无法让双方互相确认对方都「能够正常收发」。
2.2 历史请求的困扰
考虑这个场景:
-
客户端发送了一个 SYN 请求(seq=100),这个包在网络中迷路了
-
客户端超时重传了新的 SYN 请求(seq=200)
-
旧的 SYN(seq=100)最终到达服务端
-
如果只有两次握手,服务端会认为这是一个有效的连接请求并建立连接
-
而客户端并不知道这个「幽灵连接」的存在
sequenceDiagram participant C as 客户端 participant S as 服务端 Note over C,S: 场景:SYN 延迟到达 rect rgb(255, 230, 230) C->>S: SYN seq=100 (迷路) end C->>S: SYN seq=200 (重传) S->>C: SYN ACK seq=?, ack=201 Note over C,S: 客户端认为连接已建立,发送数据 S->>C: 数据 rect rgb(230, 255, 230) Note over S: 旧 SYN 到达,服务端错误建立连接 end
2.3 两次握手无法同步初始序列号
TCP 的可靠传输依赖于序列号(Sequence Number)。每个字节的数据都有一个序列号,用于:
-
可靠性:检测丢失的数据包
-
有序性:保证数据按顺序到达
-
双工通信:全双工意味着双方都要有序列号
三次握手中的第二次握手,服务端要将自己的初始序列号(Initial Sequence Number,ISN 告知客户端。第三次握手时,客户端也要将自己的 ISN 告知服务端。只有这样,双方才能正确地:
-
对发送的数据进行编号
-
对接收的数据进行确认
-
处理乱序到达的数据包
三、为什么不需要四次握手?
既然三次握手都各有其道理,为什么不是四次、五次?能否更少?
3.1 四次握手是否必要?
sequenceDiagram participant C as 客户端 participant S as 服务端 C->>S: SYN seq=x S->>C: ACK ack=x+1 S->>S: (服务端处理) S->>C: SYN seq=y C->>S: ACK ack=y+1 Note over C,S: 需要 4 次交互
四次握手在理论上是可行的,但效率低下。关键在于:服务端的 SYN 和 ACK 可以合并成一次传输。
3.2 合并的艺术
在 TCP 的状态转换中,服务端收到客户端的 SYN 后,可以立即发送 SYN+ACK(其中 ACK 是对客户端 SYN 的确认,SYN 是服务端自己的初始化序列号)。这两次信息可以同时发送,因此两次交互变成了三次。
flowchart LR subgraph 两次握手的问题 A[客户端发送 SYN] --> B[服务端发送 ACK] B --> C[服务端发送 SYN] C --> D[客户端发送 ACK] end subgraph 三次握手的优化 E[客户端发送 SYN] --> F[服务端发送 SYN+ACK] F --> G[客户端发送 ACK] end
核心洞察:服务端对客户端 SYN 的「确认」和 服务端自己的「SYN」在时间上紧密相关,合并发送是自然的优化。
四、三次握手的深层设计哲学
4.1 最小化连接建立时间
网络通信中,延迟是敌人。三次握手是最小化的、能够建立可靠全双工连接的交互次数:
-
一次交互:只能单向通信(类似 UDP)
-
两次交互:无法同步双方的序列号
-
三次交互:刚好满足所有需求
-
四次及以上:浪费,不需要
flowchart TD
A[需要什么?] --> B{能否单向?}
B -->|否| C{需要同步双方序列号?}
B -->|是| D[一次握手]
C -->|否| E[两次握手]
C -->|是| F{能否合并?}
F -->|ACK+SYN| G[三次握手]
F -->|不能合并| H[四次握手]
4.2 状态机的精妙设计
TCP 的连接管理通过状态机来实现:
flowchart LR subgraph 客户端状态 C1[CLOSED] --> C2[SYN_SENT] C2 --> C3[ESTABLISHED] end subgraph 服务端状态 S1[CLOSED] --> S2[LISTEN] S2 --> S3[SYN_RCVD] S3 --> S4[ESTABLISHED] end C1 --"主动打开"--> S2 C2 --"收到 SYN"--> S3 S3 --"收到 ACK"--> C3
三次握手完美对应了状态机的转换:
-
客户端发送 SYN → 客户端进入 SYN_SENT,服务端进入 SYN_RCVD
-
服务端发送 SYN+ACK → 双方都看到了对方的「我准备好了」
-
客户端发送 ACK → 双方都确认了对方知道自己准备好了
4.3 防御性的设计
TCP 的设计考虑到了网络的不可靠性:
| 设计要素 | 作用 |
| --------------------- | ---------------------- |
| 序列号随机化 | 防止攻击者猜测序列号 |
| 超时重传 | 处理丢包 |
| 最大报文段寿命(MSS) | 防止迷途数据包永久存活 |
| 3-way handshake | 确保双方都能收发 |
五、半关闭与四次挥手
既然提到了三次握手,就不得不提 TCP 连接的关闭——四次挥手(Four-way Wave)。
5.1 为什么关闭需要四次?
sequenceDiagram participant C as 客户端 participant S as 服务端 C->>S: FIN seq=u S->>C: ACK ack=u+1 Note over S: 客户端已无数据发送
但服务端可能还有数据要发 S->>C: FIN seq=v C->>S: ACK ack=v+1 Note over C,S: 连接关闭
关闭连接时,需要确保双方都没有数据要发送了。TCP 是全双工的,每个方向都必须单独关闭:
-
客户端发送 FIN,表示「我这边不会发送新数据了」
-
服务端收到 FIN,回复 ACK,表示「我收到了」
-
但此时服务端可能还有数据要发送给客户端,所以不能同时发送自己的 FIN
-
服务端发送完数据后,才发送自己的 FIN
-
客户端收到后回复 ACK
5.2 半关闭状态
TCP 允许连接的一端在接收完数据后单独关闭发送功能,这称为「半关闭」:
flowchart LR A[全双工] -->|close write| B[半双工] B -->|close read| C[完全关闭]
使用 shutdown() 函数可以实现半关闭,而 close() 会同时关闭读和写。
六、性能与安全的权衡
6.1 SYN Flood 攻击
三次握手设计的一个副作用是:服务端在收到客户端的 SYN 后,会分配资源并进入 SYN_RCVD 状态,等待客户端的 ACK。
攻击者发送大量 SYN 包但不完成握手,就会耗尽服务端的连接资源:
# SYN Flood 示意
for i in $(seq 1 100000); do
curl -S http://target.com &
done
bash防御手段包括:
-
SYN Cookie:不存储半开连接,而是将信息编码在序列号中
-
SYN Proxy:使用代理来验证 SYN 的真实性
-
减少 SYN_RCVD 超时时间
6.2 快速打开(TCP Fast Open)
为了减少连接建立的开销,TCP Fast Open(TFO)允许在第一次握手时就交换数据:
sequenceDiagram participant C as 客户端 participant S as 服务端 C->>S: SYN + Cookie + HTTP请求 S->>C: SYN ACK + 响应数据 Note over C,S: 数据交换在第一次交互完成
但这需要在之前已经建立过连接并获得过 Cookie,安全性需要仔细考虑。
七、总结
TCP 三次握手的设计完美地平衡了多个目标:
| 目标 | 三次握手如何实现 |
| ------ | ---------------------- |
| 可靠性 | 通过序列号确认数据接收 |
| 全双工 | 双方都能发送和接收 |
| 效率 | 最小化交互次数 |
| 灵活性 | 支持半关闭等高级特性 |
| 安全性 | 序列号随机化防御攻击 |
理解三次握手的设计原理,不仅有助于网络编程,更是理解分布式系统可靠性的起点。在设计高并发系统时,这些原理依然有重要的指导意义。
参考引用
-
RFC 793: Transmission Control Protocol — TCP 协议标准
-
RFC 7414: A Roadmap for TCP Specification Documents — TCP 规范文档路线图
-
TCP 的那些事儿(上) — 详解 TCP 三次握手四次挥手
评论区
文明评论,共建和谐社区